using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.ComponentModel;
using System.Net;
using System.IO;
using System.Diagnostics;

using Yorrick.FlickrMetadataSynchr.Events;
using System.Globalization;

namespace Yorrick.FlickrMetadataSynchr.Flickr
{
    /// <summary>
    /// Class for downloading pictures and saving them to disk.
    /// </summary>
    /// <remarks>This class implements a time-out feature to prevent the application from hanging
    /// if the download stalls for some reason.</remarks>
    internal class PictureDownloader
    {
        #region Inner class DownloadSynchronizationInfo
        /// <summary>
        /// Inner class to contain info for thread synchronization for the asynchronous download of pictures.
        /// </summary>
        private class DownloadSynchronizationInfo
        {
            /// <summary>
            /// Whether or not the download finished successfully.
            /// </summary>
            public bool Finished;
            /// <summary>
            /// The file name (including path) that should be created.
            /// </summary>
            public string FileName;
            /// <summary>
            /// The exception that occurred, or <c>null</c> when no exception occurred.
            /// </summary>
            public Exception Error;
            /// <summary>
            /// The thread synchronization object.
            /// </summary>
            public ManualResetEvent ManualResetEvent = new ManualResetEvent(false);
        }
        #endregion

        #region Private fields
        /// <summary>
        /// The maximum time allowed for downloading a picture in seconds.
        /// </summary>
        private int _pictureDownloadTimeoutInSeconds;
        #endregion

        #region Public methods
        /// <summary>
        /// Downloads a picture in a cancelable way.
        /// </summary>
        /// <param name="pictureUrl">The URL of the picture.</param>
        /// <param name="fileName">The name of the file (including path) under which
        /// to save the picture.</param>
        /// <param name="shouldCancelCallback">A delegate that will be called periodically to check for cancellation.</param>
        /// <returns><c>True</c> when the download completed, <c>false</c> otherwise.</returns>
        /// <remarks>This method is implemented asynchronously with a time-out to prevent application hangs
        /// when the download stalls.</remarks>
        /// <exception cref="FlickrHelperException">Thrown when something goes wrong.</exception>
        public bool Download(string pictureUrl, string fileName, ShouldCancelDelegate shouldCancelCallback)
        {
            if (File.Exists(fileName))
            {
                string errorMessage = String.Format(CultureInfo.CurrentCulture, Properties.Messages.DownloadFileAlreadyExists, fileName);

                throw new FlickrHelperException(errorMessage);
            }

            using (WebClient webClient = new WebClient())
            {
                return DownloadInternal(pictureUrl, fileName, shouldCancelCallback, webClient);
            }
        }

        /// <summary>
        /// Downloads a picture in a cancelable way.
        /// </summary>
        /// <param name="pictureUrl">The URL of the picture.</param>
        /// <param name="fileName">The name of the file (including path) under which
        /// to save the picture.</param>
        /// <param name="shouldCancel">A delegate that will be called periodically to check for cancellation.</param>
        /// <param name="webClient">The <see cref="WebClient"/> to use for downloading.</param>
        /// <returns><c>True</c> when the download completed, <c>false</c> otherwise.</returns>
        /// <remarks>This method is implemented asynchronously with a time-out to prevent application hangs
        /// when the download stalls.</remarks>
        /// <exception cref="FlickrHelperException">Thrown when something goes wrong.</exception>
        private bool DownloadInternal(string pictureUrl, string fileName, ShouldCancelDelegate shouldCancelCallback, WebClient webClient)
        {
            DownloadSynchronizationInfo info = new DownloadSynchronizationInfo();
            info.FileName = fileName;

            using (info.ManualResetEvent)
            {
                webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
                webClient.DownloadFileAsync(new Uri(pictureUrl), fileName, info);

                int timeInSeconds = 0;

                bool continueWaiting = true;
                bool timeOut = false;
                bool finished = false;

                while (continueWaiting)
                {
                    // Wait for one second and then check if downloading is done or should be timed out.
                    finished = info.ManualResetEvent.WaitOne(1000, false);
                    timeOut = (timeInSeconds++ > _pictureDownloadTimeoutInSeconds);
                    bool shouldCancel = shouldCancelCallback();
                    continueWaiting = !finished && !timeOut && !shouldCancel;
                }

                // Make sure the info DownloadSynchronizationInfo is accessed in a thread safe way by locking it.
                lock (info)
                {
                    return HandleDownloadFinish(webClient, info, timeOut, finished);
                }
            }
        }

        /// <summary>
        /// Handles ending the download operation, regardless of because it's done, an error occured, the download has timed out or
        /// the user has canceled the download.
        /// </summary>
        /// <param name="webClient">The <see cref="WebClient"/> used for the async download operation.</param>
        /// <param name="info">The structure containing synchronization information.</param>
        /// <param name="timeOut">Whether or not the operation has timed out.</param>
        /// <param name="finished">Whether or not the download has finished successfully.</param>
        /// <returns><c>True</c> if the download finished successfully, <c>false</c> otherwise.</returns>
        private static bool HandleDownloadFinish(WebClient webClient, DownloadSynchronizationInfo info, bool timeOut, bool finished)
        {
            // Null out the ManualResetEvent because the other thread might still be executing
            // and can still try to signal the event. This will cause an exception
            // if this happens after the ManualResetEvent has been disposed.
            info.ManualResetEvent = null;

            Exception downloadException = info.Error;

            // If it is not finished, try to cancel the download request and throw an exception
            // indicating time-out to the caller of this method.
            if (finished)
            {
                return true;
            }

            try
            {
                // Watch out, this call might change info.Error synchronously!
                webClient.CancelAsync();
            }
            catch
            { }

            if (timeOut)
            {
                throw new FlickrHelperException(Properties.Messages.DownloadPictureTimeoutError);
            }

            // If a download error occurred, throw it to notify the caller of this method.
            if (downloadException != null)
            {
                throw downloadException;
            }

            return false;
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Callback for when a picture is downloaded or when an error occured.
        /// </summary>
        /// <param name="sender">Not used.</param>
        /// <param name="e">The event arguments.</param>
        private void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
        {
            DownloadSynchronizationInfo downloadSynchronizationInfo = (DownloadSynchronizationInfo)e.UserState;

            Exception downloadException = e.Error;

            if (downloadException is WebException)
            {
                if (downloadException.InnerException == null || downloadException.InnerException is WebException)
                {
                    downloadException = FlickrHelperException.GetDownloadSaveException(downloadException);
                }
                else
                {
                    downloadException = FlickrHelperException.GetDownloadSaveException(downloadException.InnerException);
                }
            }

            // If anything went wrong, try to delete the file because it might contain a partial image.
            // Keeping the file might cause trouble if we try to read its metadata in a next run of the app.
            if (downloadException != null)
            {
                try
                {
                    File.Delete(downloadSynchronizationInfo.FileName);
                }
                catch (Exception deleteEx)
                {
                    Trace.TraceWarning("Failed to delete partially downloaded file '" + downloadSynchronizationInfo.FileName + "'. Exception details follow:"
                       + System.Environment.NewLine + deleteEx.ToString());
                }
            }

            // Make sure the info DownloadSynchronizationInfo is accessed in a thread safe way by locking it.
            lock (downloadSynchronizationInfo)
            {
                downloadSynchronizationInfo.Error = downloadException;
                downloadSynchronizationInfo.Finished = true;

                if (downloadSynchronizationInfo.ManualResetEvent != null)
                {
                    downloadSynchronizationInfo.ManualResetEvent.Set();
                }
            }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Constructs a new instance. Initializes settings from configuration.
        /// </summary>
        public PictureDownloader()
        {
            try
            {
                _pictureDownloadTimeoutInSeconds = Properties.Settings.Default.PictureDownloadTimeoutInSeconds;
            }
            catch (InvalidCastException ex)
            {
                throw FlickrHelperException.GetInvalidConfigurationSettingException("PictureDownloadTimeoutInSeconds", ex);
            }
        }
        #endregion
    }
}
